/*
 * Copyright 2019 New Vector Ltd
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package im.vector.util

import android.text.Spannable
import android.text.SpannableString
import android.text.method.LinkMovementMethod
import android.text.style.URLSpan
import android.text.util.Linkify
import android.widget.TextView
import androidx.core.text.util.LinkifyCompat
import java.util.*

/**
 * Better support for auto link than the default implementation
 */
fun TextView.vectorCustomLinkify(keepExistingUrlSpan: Boolean = false) {

    val spannableText = SpannableString.valueOf(text)

    //we might want to modify some matches
    val createdSpans = ArrayList<LinkSpec>()

    if (keepExistingUrlSpan) {
        //Keep track of existing URLSpans, and mark them as important
        spannableText.forEachSpanIndexed { _, urlSpan, start, end ->
            createdSpans.add(LinkSpec(URLSpan(urlSpan.url), start, end, important = true))
        }
    }

    //Use the framework first, the found span can then be manipulated if needed
    LinkifyCompat.addLinks(spannableText, Linkify.WEB_URLS or Linkify.EMAIL_ADDRESSES or Linkify.PHONE_NUMBERS)

    //we might want to modify some matches
    spannableText.forEachSpanIndexed { _, urlSpan, start, end ->
        spannableText.removeSpan(urlSpan)

        //remove short PN, too much false positive
        if (urlSpan.url?.startsWith("tel:") == true) {
            if (end - start > 6) { //Do not match under 7 digit
                createdSpans.add(LinkSpec(URLSpan(urlSpan.url), start, end))
            }
            return@forEachSpanIndexed
        }

        //include mailto: if found before match
        if (urlSpan.url?.startsWith("mailto:") == true) {
            val protocolLength = "mailto:".length
            if (start - protocolLength >= 0 && "mailto:" == spannableText.substring(start - protocolLength, start)) {
                //modify to include the protocol
                createdSpans.add(LinkSpec(URLSpan(urlSpan.url), start - protocolLength, end))
            } else {
                createdSpans.add(LinkSpec(URLSpan(urlSpan.url), start, end))
            }

            return@forEachSpanIndexed
        }

        //Handle url matches

        //check trailing space
        if (end < spannableText.length - 1 && spannableText[end] == '/') {
            //modify the span to include the slash
            val spec = LinkSpec(URLSpan(urlSpan.url + "/"), start, end + 1)
            createdSpans.add(spec)
            return@forEachSpanIndexed
        }
        //Try to do something for ending ) issues/3020
        if (spannableText[end - 1] == ')') {
            var lbehind = end - 2
            var isFullyContained = 1
            while (lbehind > start) {
                val char = spannableText[lbehind]
                if (char == '(') isFullyContained -= 1
                if (char == ')') isFullyContained += 1
                lbehind--
            }
            if (isFullyContained != 0) {
                //In this case we will return false to match, and manually add span if we want?
                val span = URLSpan(spannableText.substring(start, end - 1))
                val spec = LinkSpec(span, start, end - 1)
                createdSpans.add(spec)
                return@forEachSpanIndexed
            }
        }

        createdSpans.add(LinkSpec(URLSpan(urlSpan.url), start, end))
    }

    LinkifyCompat.addLinks(spannableText, VectorAutoLinkPatterns.GEO_URI, "geo:", arrayOf("geo:"), geoMatchFilter, null)
    spannableText.forEachSpanIndexed { _, urlSpan, start, end ->
        spannableText.removeSpan(urlSpan)
        createdSpans.add(LinkSpec(URLSpan(urlSpan.url), start, end))
    }

    pruneOverlaps(createdSpans)
    for (spec in createdSpans) {
        spannableText.setSpan(spec.span, spec.start, spec.end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
    }

    text = spannableText
    addLinkMovementMethod()
}

private fun pruneOverlaps(links: ArrayList<LinkSpec>) {
    Collections.sort(links, COMPARATOR)
    var len = links.size
    var i = 0
    while (i < len - 1) {
        val a = links[i]
        val b = links[i + 1]
        var remove = -1

        //test if there is an overlap
        if (b.start in a.start until a.end) {
            if (a.important != b.important) {
                remove = if (a.important) i + 1 else i
            } else {
                when {
                    b.end <= a.end ->
                        //b is inside a -> b should be removed
                        remove = i + 1
                    a.end - a.start > b.end - b.start ->
                        //overlap and a is bigger -> b should be removed
                        remove = i + 1
                    a.end - a.start < b.end - b.start ->
                        //overlap and a is smaller -> a should be removed
                        remove = i
                }
            }

            if (remove != -1) {
                links.removeAt(remove)
                len--
                continue
            }
        }
        i++
    }
}


private data class LinkSpec(val span: URLSpan,
                            val start: Int,
                            val end: Int,
                            val important: Boolean = false)

private val COMPARATOR = Comparator<LinkSpec> { (_, startA, endA), (_, startB, endB) ->
    if (startA < startB) {
        return@Comparator -1
    }

    if (startA > startB) {
        return@Comparator 1
    }

    if (endA < endB) {
        return@Comparator 1
    }

    if (endA > endB) {
        -1
    } else 0
}

//Exclude short match that don't have geo: prefix, e.g do not highlight things like 1,2
private val geoMatchFilter = Linkify.MatchFilter { s, start, end ->
    if (s[start] != 'g') { //doesn't start with geo:
        return@MatchFilter end - start > 12
    }
    return@MatchFilter true
}

private inline fun Spannable.forEachSpanIndexed(action: (index: Int, urlSpan: URLSpan, start: Int, end: Int) -> Unit) {
    getSpans(0, length, URLSpan::class.java)
            .forEachIndexed { index, urlSpan ->
                val start = getSpanStart(urlSpan)
                val end = getSpanEnd(urlSpan)
                action.invoke(index, urlSpan, start, end)
            }
}

private fun TextView.addLinkMovementMethod() {
    val m = movementMethod

    if (m == null || m !is LinkMovementMethod) {
        if (linksClickable) {
            movementMethod = LinkMovementMethod.getInstance()
        }
    }
}
